Advisor的核心实现

Advisor家族

核心族谱

  • 下图是官方给出的Advisor类图

    Advisors API 类

  • 官方的图因为版本的原因,类名上有些出入。下面是在IDEA生成的类图,一起结合看一下。

    IDEA类图

  • 可以看到最上层接口是Ordered,前面也说到过是排序。排序规则是,数值越小优先级越高。它具备两个常量,相当于最高和最低优先级。

    1
    2
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
  • Advisor接口只有一个getName,核心还是CallAdvisor, StreamAdvisor两个接口,所以核心方法也就是adviseCalladviseStream两个方法了。

  • 然后就是CallAdvisorChainStreamAdvisorChain,从上图可以看出与CallAdvisor, StreamAdvisor是一个对应的聚合关系,组成了Advisor的调用链。

  • ChatClientRequest就是请求,ChatClientRequest可以获取到ChatCLient调用AI的提示词、上下文等,还可以进行复制和异变操作,这些在后续会继续记录。

  • 最后是ChatClientResponse响应了。ChatClientResponse在之前的“SpringAI - ChatClient(二)”的“更多响应类型”文中就记录过了。

前置&后置处理

  • 这里有一点在上一篇LogAdvisor案例中没体现出来的,其实在调用callAdvisorChain.nextCall(chatClientRequest);后可以再继续做一些处理。如下:

    1
    2
    3
    4
    5
    6
    7
    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    log.info("\nadviseCall prompt -> {}", chatClientRequest.prompt().getContents());
    ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
    log.info("\nadviseCall response text-> {}", chatClientResponse.chatResponse().getResult().getOutput().getText());
    return chatClientResponse;
    }
  • adviseCall中改造了一下,然后看看输出结果

    1
    2
    3
    4
    5
    2025-06-27T12:19:28.058+08:00  INFO 82818 --- [spring-ai-example] [           main] c.s.ai.example.advisor.one.LogAdvisor    : 
    adviseCall prompt -> 你是一个Java专家,请帮忙解答提出的Java相关问题。Java 之父的全称叫什么?

    2025-06-27T12:19:36.455+08:00 INFO 82818 --- [spring-ai-example] [ main] c.s.ai.example.advisor.one.LogAdvisor :
    adviseCall response text-> Java 之父的全称是 **詹姆斯·高斯林(James Gosling)**,通常被称为 **"Java 之父"(Father of Java)**。
  • 到这里就能看出来为什么官方的图中名称叫xxxAroundAdvisor了吧,那是不是更觉得有些眼熟了啊?

Advisor执行流程

执行流程

  • 先看看官方给的执行流程图,由于版本的关系略有出入,参考帮助理解即可。

    执行流程

  • 第一步先是封装ChatClientRequest,从代码中看到是封装了提示词和上下文。以及复制和异变的操作,这个后续在记录。

    1
    2
    Prompt prompt = chatClientRequest.prompt();
    Map<String, Object> context = chatClientRequest.context();
  • 第二步执行整个链中Advisor的前置动作,在代码中体现出来的就是callAdvisorChain.nextCall(chatClientRequest);前的逻辑处理。顺序按getOrder的升序执行。

  • 第三步和第四步是执行调用AI与AI通信得到输出响应,下面详细记录。

  • 第五步在得到响应后,执行链中Advisor的后置动作,即callAdvisorChain.nextCall(chatClientRequest);后的逻辑处理。顺序则与第二步相反,是按getOrder的降序执行。

  • 最后将响应返回给ChatClient调用处。

如何与AI通信的

  • 其实也是通过一个Advisor来执行的,只是这个Advisor不需要显示的添加到ChatClient,默认就会有的。

  • 实现类分别是ChatModelCallAdvisorChatModelStreamAdvisor,是SpringAI封装好的,默认优先级是最低的,也就是 Ordered.LOWEST_PRECEDENCE

  • 记录踩坑:

    • 在写LogAdvisor案例的时候,给的排序也是 Ordered.LOWEST_PRECEDENCE,然后发现并没有执行LogAdvisor
    • 检查了很多地方,发现写的并没有问题,通过debug源码后看到在链中还有有一个ChatModelCallAdvisor
    • 且发现执行完ChatModelCallAdvisor链中后续的Advisor就不会再执行了。
    • 所以其他的Advisor的排序必须要小于Ordered.LOWEST_PRECEDENCE
  • 最后看一下ChatModelCallAdvisorChatModelStreamAdvisoradviseCalladviseStream方法中的执行

    • adviseCall

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Override
      public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
      Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");

      ChatClientRequest formattedChatClientRequest = augmentWithFormatInstructions(chatClientRequest);

      ChatResponse chatResponse = this.chatModel.call(formattedChatClientRequest.prompt());
      return ChatClientResponse.builder()
      .chatResponse(chatResponse)
      .context(Map.copyOf(formattedChatClientRequest.context()))
      .build();
      }
    • adviseStream

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Override
      public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
      StreamAdvisorChain streamAdvisorChain) {
      Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");

      return this.chatModel.stream(chatClientRequest.prompt())
      .map(chatResponse -> ChatClientResponse.builder()
      .chatResponse(chatResponse)
      .context(Map.copyOf(chatClientRequest.context()))
      .build())
      .publishOn(Schedulers.boundedElastic()); // TODO add option to disable
      }
  • 可以看到最后是通过chatModel调用的call或者stream进行与AI交互了。

执行顺序

  • 前面的记录中多次提到了顺序是有getOrder决定的。数值越小越优先执行。

  • Advisor链从链首向链尾执行请求处理(前置处理),再由链尾向链首执行响应处理(后置处理)。执行类似于一个栈结构。

  • 那么到这里,就会发现与Web中的Filter非常相似。

    1
    2
    3
    4
    5
    6
    7
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    //请求(前置)处理

    filterChain.doFilter(servletRequest, servletResponse); // 过滤器链

    //响应(后置)处理
    }

总结

  • 到这基本上对Advisor有个全面的认识了。
  • 核心是CallAdvisorStreamAdvisor接口的adviseCalladviseStream两个方法。
  • Advisor链的执行顺序类似栈结构。
  • 默认具有ChatModelCallAdvisorChatModelStreamAdvisor,并通过二者与AI进行交互。
  • 所有的Advisor的排序必须要小于Ordered.LOWEST_PRECEDENCE
  • Advisor与JavaWeb的Filter有着与曲同工之妙。

最后

  • 具体Advisor的源码分析等后面深入Spring AI整体源码学习的时候再做记录,目前还是已使用为主。
  • 下一篇会继续记录几个简单的案例来尝试拦截、增强与AI的交互。
  • 所有案例的源码,都会提交在GitHub上。